namespace ClassyBlog.Infrastructure
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;
    using System.Text.RegularExpressions;

    using ExtensionMethods;

    public class MacroRunner : IMacroRunner
    {
        private static readonly IEnumerable<string> methodNames =
            new[] { "Execute", "Process", "Render", "Run", "Write" };

        private static readonly Func<MethodInfo, bool> methodfilter =
            m => methodNames.Contains(m.Name, StringComparer.Ordinal) &&
                 !m.IsSpecialName &&
                 typeof(string).IsAssignableFrom(m.ReturnType);

        private static readonly TypeConverter stringConverter =
            TypeDescriptor.GetConverter(typeof(string));

        public MacroRunner(IEnumerable<object> macros)
        {
            if (macros == null)
            {
                throw new ArgumentNullException("macros");
            }

            Macros = macros.ToDictionary(
                GetMacroName, d => d, StringComparer.OrdinalIgnoreCase);
        }

        protected IDictionary<string, object> Macros { get; private set; }

        public virtual string Run(string raw)
        {
            var matches = RegexExpressions.MacroMarker
                                          .Matches(raw)
                                          .Cast<Match>()
                                          .Where(m => m.Success)
                                          .OrderByDescending(m => m.Index)
                                          .ToList();

            var processedContent = raw;

            foreach (var match in matches)
            {
                var preProcessedMacroContent = match.Value;

                // Remove the first and last marker
                var macroWithArguments = preProcessedMacroContent.Substring(
                    2, preProcessedMacroContent.Length - 4).Trim();

                var argumentSeparatorIndex = macroWithArguments.IndexOf(' ');

                var macroName = macroWithArguments;
                var arguments = new string[0];

                if (argumentSeparatorIndex > 0)
                {
                    macroName = macroWithArguments.Substring(
                        0, argumentSeparatorIndex).Trim();

                    var rawArguments = macroWithArguments.Substring(
                        argumentSeparatorIndex).Trim();

                    arguments = rawArguments.Split(
                        new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                }

                if (!Macros.ContainsKey(macroName))
                {
                    // if no macro is registered do nothing and try the next
                    continue;
                }

                var macro = Macros[macroName];

                var postProcessedMacroContent = ExecuteMacro(macro, arguments);

                var before = processedContent.Substring(0, match.Index);
                var after = processedContent.Substring(
                    match.Index + match.Length);

                processedContent = before + postProcessedMacroContent + after;
            }

            return processedContent;
        }

        private static string ExecuteMacro(object macro, IList<string> arguments)
        {
            var method = GetMethod(macro);

            if (method == null)
            {
                throw new InvalidOperationException(
                    TextMessages.MacroRunner_ExecuteMacro_NoMethodFound.Interpolate(
                    string.Join(", ", methodNames), macro.GetType().FullName));
            }

            var args = new List<object>();
            var parameters = method.GetParameters();

            for (var i = 0; i < parameters.Length; i++)
            {
                var arg = arguments[i].Trim();
                var parameter = parameters[i];
                TypeConverter parameterConverter;

                if (stringConverter.CanConvertTo(parameter.ParameterType))
                {
                    args.Add(stringConverter.ConvertTo(
                        arg, parameter.ParameterType));
                }
                else if (((parameterConverter =
                    TypeDescriptor.GetConverter(
                    parameter.ParameterType)) != null) &&
                    parameterConverter.CanConvertFrom(typeof(string)))
                {
                    args.Add(parameterConverter.ConvertFrom(arg));
                }
                else
                {
                    throw new InvalidOperationException(
                        TextMessages.
                        MacroRunner_ExecuteMacro_UnableToConvertParameterValue.
                        Interpolate(
                        arguments[i],
                        parameter.Name,
                        method.Name,
                        macro.GetType().FullName));
                }
            }

            var content = (string)method.Invoke(macro, args.ToArray());

            return content;
        }

        private static MethodInfo GetMethod(object macro)
        {
            const BindingFlags Flags = BindingFlags.Public |
                                       BindingFlags.Instance;

            var method = macro.GetType()
                              .GetMethods(Flags)
                              .Where(methodfilter)
                              .FirstOrDefault();

            return method;
        }

        private static string GetMacroName(object macro)
        {
            var macroType = macro.GetType();

            var attribute = macroType.GetCustomAttributes(typeof(MacroAttribute), false)
                                     .OfType<MacroAttribute>()
                                     .FirstOrDefault();

            var hasAlias = attribute != null &&
                           !string.IsNullOrWhiteSpace(attribute.Alias);

            return hasAlias ? attribute.Alias : macroType.Name;
        }
    }
}